﻿// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Quantum.QsCompiler.DataTypes;
using Microsoft.Quantum.QsCompiler.Diagnostics;
using Microsoft.Quantum.QsCompiler.SyntaxTree;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Lsp = Microsoft.VisualStudio.LanguageServer.Protocol;
using Position = Microsoft.Quantum.QsCompiler.DataTypes.Position;
using Range = Microsoft.Quantum.QsCompiler.DataTypes.Range;

namespace Microsoft.Quantum.QsCompiler.CompilationBuilder
{
    public static class DiagnosticTools
    {
        /// <summary>
        /// Returns the zero-based line and character index indicating the position of a symbol in the file.
        /// </summary>
        /// <param name="rootLocation">The location information for a declared symbol.</param>
        /// <param name="symbolPosition">The position of the declaration within which the symbol is declared.</param>
        /// <returns>
        /// The line and character index, or null if the given object is not compatible with the position
        /// information generated by this <see cref="CompilationBuilder"/>.
        /// </returns>
        public static Position SymbolPosition(QsLocation rootLocation, QsNullable<Position> symbolPosition, Range symbolRange)
        {
            // the position offset is set to null (only) for variables defined in the declaration
            var offset = symbolPosition.IsNull ? rootLocation.Offset : rootLocation.Offset + symbolPosition.Item;
            return offset + symbolRange.Start;
        }

        /// <summary>
        /// Returns a new <see cref="Diagnostic"/>, making a deep copy of <paramref name="message"/> (in particular a deep copy of it's Range).
        /// </summary>
        /// <returns>
        /// The new diagnostic, or null if <paramref name="message"/> is null.
        /// </returns>
        [return: NotNullIfNotNull("message")]
        public static Diagnostic? Copy(this Diagnostic message)
        {
            Lsp.Position CopyPosition(Lsp.Position position) =>
                position == null ? new Lsp.Position() : new Lsp.Position(position.Line, position.Character);

            Lsp.Range CopyRange(Lsp.Range range) =>
                new Lsp.Range
                {
                    Start = CopyPosition(range.Start),
                    End = CopyPosition(range.End),
                };

            // NB: The nullability metadata on Diagnostic.Range is incorrect,
            //     such that some Diagnostic values may have nullable ranges.
            //     We cannot assign that to a new Diagnostic without
            //     contradicting nullability metadata, however, so we need to
            //     explicitly disable nullable references for the following
            //     statement. Once the upstream bug in the LSP client package
            //     is fixed, we can remove the nullable disable here.
            #nullable disable
            return message is null
                ? null
                : new Diagnostic
                {
                    Range = message.Range == null ? null : CopyRange(message.Range),
                    Severity = message.Severity,
                    Code = message.Code,
                    Source = message.Source,
                    Message = message.Message,
                };
            #nullable restore
        }

        /// <summary>
        /// Returns a copy of <paramref name="diagnostic"/> with <paramref name="offset"/> added to the line numbers.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The new diagnostic would have negative line numbers.
        /// </exception>
        public static Diagnostic WithLineNumOffset(this Diagnostic diagnostic, int offset)
        {
            var copy = diagnostic.Copy();

            // NB: Despite the nullability metadata, Range may be null here.
            //     We thus need to guard accordingly.
            if (copy.Range != null)
            {
                copy.Range.Start.Line += offset;
                copy.Range.End.Line += offset;
                if (copy.Range.Start.Line < 0 || copy.Range.End.Line < 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(offset), "Translated diagnostic has negative line numbers.");
                }
            }

            return copy;
        }

        /// <summary>
        /// Returns a function that returns true if the <see cref="ErrorType"/> of the
        /// given <see cref="Diagnostic"/> exists in <paramref name="types"/>.
        /// </summary>
        public static Func<Diagnostic, bool> ErrorType(params ErrorCode[] types)
        {
            var codes = types.Select(err => err.Code());
            return m => m.IsError() && codes.Contains(m.Code);
        }

        /// <summary>
        /// Returns a function that returns true if the <see cref="WarningType"/> of the
        /// given <see cref="Diagnostic"/> exists in <paramref name="types"/>.
        /// </summary>
        public static Func<Diagnostic, bool> WarningType(params WarningCode[] types)
        {
            var codes = types.Select(warn => warn.Code());
            return m => m.IsWarning() && codes.Contains(m.Code);
        }

        /// <summary>
        /// Returns true if <paramref name="m"/> is an error.
        /// </summary>
        public static bool IsError(this Diagnostic m) =>
            m.Severity == DiagnosticSeverity.Error;

        /// <summary>
        /// Returns true if <paramref name="m"/> is a warning.
        /// </summary>
        public static bool IsWarning(this Diagnostic m) =>
            m.Severity == DiagnosticSeverity.Warning;

        /// <summary>
        /// Returns true if <paramref name="m"/> is an information.
        /// </summary>
        public static bool IsInformation(this Diagnostic m) =>
            m.Severity == DiagnosticSeverity.Information;

        /// <summary>
        /// Extracts all elements satisfying <paramref name="condition"/> and which start
        /// at a line that is larger or equal to <paramref name="lowerBound"/>.
        /// </summary>
        /// <remarks>
        /// Diagnostics without any range information are only extracted if no lower bound is specified or the specified lower bound is smaller than zero.
        /// </remarks>
        [return: NotNullIfNotNull("orig")]
        public static IEnumerable<Diagnostic>? Filter(this IEnumerable<Diagnostic>? orig, Func<Diagnostic, bool> condition, int lowerBound = -1) =>
            orig?.Where(m => condition(m) && lowerBound <= (m.Range?.Start?.Line ?? -1));

        /// <summary>
        /// Extracts all elements satisfying <paramref name="condition"/> and which start at a line
        /// that is larger or equal to <paramref name="lowerBound"/> and smaller than <paramref name="upperBound"/>.
        /// </summary>
        [return: NotNullIfNotNull("orig")]
        public static IEnumerable<Diagnostic>? Filter(this IEnumerable<Diagnostic>? orig, Func<Diagnostic, bool> condition, int lowerBound, int upperBound) =>
            orig?.Where(m => condition(m) && lowerBound <= m.Range.Start.Line && m.Range.End.Line < upperBound);

        /// <summary>
        /// Extracts all elements which start at a line that is larger or equal to <paramref name="lowerBound"/>.
        /// </summary>
        [return: NotNullIfNotNull("orig")]
        public static IEnumerable<Diagnostic>? Filter(this IEnumerable<Diagnostic> orig, int lowerBound)
        {
            return orig?.Filter(m => true, lowerBound);
        }

        /// <summary>
        /// Extracts all elements which start at a line that is larger or equal to
        /// <paramref name="lowerBound"/> and smaller than <paramref name="upperBound"/>.
        /// </summary>
        [return: NotNullIfNotNull("orig")]
        public static IEnumerable<Diagnostic>? Filter(this IEnumerable<Diagnostic> orig, int lowerBound, int upperBound)
        {
            return orig?.Filter(m => true, lowerBound, upperBound);
        }

        /// <summary>
        /// Returns true if the start line of <paramref name="m"/> is larger or equal to <paramref name="lowerBound"/>.
        /// </summary>
        internal static bool SelectByStartLine(this Diagnostic m, int lowerBound)
        {
            return m?.Range?.Start?.Line == null ? false : lowerBound <= m.Range.Start.Line;
        }

        /// <summary>
        /// Returns true if the start line of <paramref name="m"/> is larger or equal
        /// to <paramref name="lowerBound"/>, and smaller than <paramref name="upperBound"/>.
        /// </summary>
        internal static bool SelectByStartLine(this Diagnostic m, int lowerBound, int upperBound)
        {
            return m?.Range?.Start?.Line == null ? false : lowerBound <= m.Range.Start.Line && m.Range.Start.Line < upperBound;
        }

        /// <summary>
        /// Returns true if the end line of <paramref name="m"/> is larger or equal
        /// to <paramref name="lowerBound"/>, and smaller than <paramref name="upperBound"/>.
        /// </summary>
        internal static bool SelectByEndLine(this Diagnostic m, int lowerBound, int upperBound)
        {
            return m?.Range?.End?.Line == null ? false : lowerBound <= m.Range.End.Line && m.Range.End.Line < upperBound;
        }

        /// <summary>
        /// Returns true if the start position of <paramref name="m"/> is larger or equal to <paramref name="lowerBound"/>.
        /// </summary>
        internal static bool SelectByStart(this Diagnostic m, Position lowerBound) =>
            m?.Range?.Start?.Line != null && lowerBound <= m.Range.Start.ToQSharp();

        /// <summary>
        /// Returns true if the start position of the range of <paramref name="m"/> is
        /// contained in <paramref name="range"/>, excluding that range's end position.
        /// </summary>
        internal static bool SelectByStart(this Diagnostic m, Range range) =>
            !(m?.Range?.Start is null) && range.Contains(m.Range.Start.ToQSharp());

        /// <summary>
        /// Returns true if the end position of <paramref name="m"/> is larger or equal to <paramref name="lowerBound"/>.
        /// </summary>
        internal static bool SelectByEnd(this Diagnostic m, Position lowerBound) =>
            m?.Range?.End?.Line != null && lowerBound <= m.Range.End.ToQSharp();

        /// <summary>
        /// Returns true if the end position of the range of <paramref name="m"/> is
        /// contained in <paramref name="range"/>, excluding that range's end position.
        /// </summary>
        internal static bool SelectByEnd(this Diagnostic m, Range range) =>
            !(m?.Range?.End is null) && range.Contains(m.Range.End.ToQSharp());
    }
}
